前幾篇我們有提到useState的setter function會觸發重新渲染這個機制,這篇我們會更詳細說明渲染的細節。
首先們先回顧一下整個更新畫面的步驟
在呼叫setter function後,不是直接觸發重新渲染,也不會馬上更新狀態值,而是將setter要執行的運算加入到queue,待event handler其他程式執行完再批次執行queue與使用新的狀態渲染,最後完成畫面更新。每次渲染完JSX都會產生一個快照(snapshot),這個快照會有當下的狀態、事件綁定,呈現的UI會根據這次的狀態完成更新。
以下看個範例會更容易理解
import { useState } from "react";
function App() {
const [val, setVal] = useState(0);
return (
<>
<h2 id="val">{val}</h2>
<button id="btn" onClick={() => {
setVal(val + 1);
setVal(val + 1);
setVal(val + 1);
}}>+3</button>
</>
)
}
export default App
當你點擊1次+3時,結果會是1不是3,因為狀態會根據快照的值去運算且不會即時更新。
因為val的初始值是0,所以setVal實際上是
setVal(0 + 1)
setVal(0 + 1)
setVal(0 + 1)
根據上述val的結果會是1不會是3,然後React會將這個結果1交給下個渲染產生另個快照。
如果想要結果是3的話可以使用帶入function的方式,並使用預設function帶入的參數-前一個操作執行的結果
import { useState } from "react";
function App() {
const [val, setVal] = useState(0);
return (
<>
<h2 id="val">{val}</h2>
<button id="btn" onClick={() => {
setVal(preVal => preVal + 1);
setVal(preVal => preVal + 1);
setVal(preVal => preVal + 1);
}}>+3</button>
</>
)
}
export default App
這次點擊+3的結果就會是3,實際上的執行如下
setVal(0 => 0 + 1);
setVal(1 => 1 + 1);
setVal(2 => 2 + 1);
所以這次的結果就會是3。
import { useState } from "react";
function App() {
const [val, setVal] = useState(0);
return (
<>
<h2 id="val">{val}</h2>
<button id="btn" onClick={() => {
setVal(preVal => preVal + 1);
setVal(preVal => preVal + 1);
setVal(preVal => preVal + 1);
alert(val);
}}>+3</button>
</>
)
}
export default App
我們可以用個alert來驗證val的值,結果會是原始快照的0,並且是先跳出alert後才渲染並更新畫面。
再來另個例子,如果在非同步的情況也會有什麼樣的結果
import { useState } from "react";
function App() {
const [val, setVal] = useState(0);
return (
<>
<h2 id="val">{val}</h2>
<button id="btn" onClick={() => {
setVal(preVal => preVal + 1);
setVal(preVal => preVal + 1);
setVal(preVal => preVal + 1);
setTimeout(() => {
alert(val);
}, 3000);
}}>+3</button>
</>
)
}
export default App
alert經過3秒後一樣是跳出0。
這結果跟我們在JS上的不太一樣,如果是在JS結果會是3
let v = 0;
v = v + 1;
v = v + 1;
v = v + 1;
setTimeout(() => {
alert(v);
}, 3000);
這是因為在React不管是不是同步還非同步,都會保留原本快照的狀態進行運算。
這邊還有個官方文件提出的範例,在alert出現前使用者更動alert的值,我們來看看結果
和之前的結果一樣,會咬住一開始快照的狀態,不會因為操作而改變。
我們在一開始有提到在觸發事件後執行的順序,以這個範例為例
import { useState } from "react";
function App() {
const [val, setVal] = useState(0);
function clickHandler() {
setVal(n => {
console.log(1);
return n + 1;
})
setVal(n => {
console.log(2);
return n + 1;
})
setVal(n => {
console.log(3);
return n + 1;
})
console.log('other action')
}
return (
<>
<h2 id="val">{val}</h2>
<button id="btn" onClick={clickHandler}>+3</button>
</>
)
}
export default App
使用者操作觸發event handler→將setVal放到queue→執行alert→執行queue裡的程式→渲染→產生virtual DOM並與上一版的比較(這邊可以先忽略之後會再介紹)→更新畫面
我在第一次執行點擊的時候不會是這個流程?但是之後就會依照這個文件上的流程,為什麼會有這樣的結果?歡迎跟我分享原因
https://react.dev/learn/state-as-a-snapshot